/******************************************************/
/* Goblin's Cube - Rubik's cube game for Wii Homebrew */
/* Copyright (C) 2009 Ricardo Bueno Cordeiro          */
/* Licensed under de GNU General Public License v2.0  */
/*   See license_gplv2.txt for detalis                */
/******************************************************/

#include <gccore.h>
#include <fat.h>
#include <stdio.h>
#include <unistd.h>
#include <network.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>

#include "draw.h"
#include "network.h"
#include "highscore.h"
#include "filesystem.h"

#define HTTP_HOST "games.nextis.com"
#define SERVER_PATH "/goblin"

static const char game_folder[] = "/apps/goblin";

static const char local_highscore_filename[] = "goblins_cube.data";
static const char online_load[] = "GET " SERVER_PATH "/load.php HTTP/1.1\r\nHost: " HTTP_HOST "\r\nConnection: close\r\n\r\n";
static const char online_save[] = "GET " SERVER_PATH "/save.php?initial=%c%c%c&time=%d HTTP/1.1\r\nHost: " HTTP_HOST "\r\nConnection: close\r\n\r\n";
static const char host_name[] = HTTP_HOST;
static const u16 host_port = 80;

int Highscore::parse_buffer(char *online_buffer, char *response) {
	char *temp = online_buffer;
	
	while (*temp) {
		*temp = toupper(*temp);
		temp++;
	}

	char *encoding = strstr(online_buffer, "TRANSFER-ENCODING: CHUNKED");
	char *start = strstr(online_buffer, "\r\n\r\n");

	if (start == NULL) {
		this->num = 0;
		return 0;
	}
	
	start += 4;
	
	if (encoding == NULL) {
		strcpy(response, start);
		return 1;
	}
	
	s32 length;

	char *data;
		
	while(*start) {
		length = strtol(start, &data, 16);

		if (start == data)
			return 0;

		if (!length) {
			*response = '\0';
			return 1;
		}
			
		while ((*data != '\n') && (*data != '\0')) data++;
		if (*data == '\0')
			return 0;

		data++;
		
		memcpy(response, data, length);
		response += length;

		start = data;

		while ((length--) && (*start))
			start++;
	}

	return 0;
}

void Highscore::parse_response(const char *response) {
	Lock lock(this->mutex, this->mutex_status);

	char *current, *end;
	
	this->num = strtol(response, &current, 10);
	if (current == response) {
		this->num = 0;
		return;
	}

	while ((*current != '\n') && (*current != '\0')) current++;

	if (*current != '\0')
		current++;

	for (int i = 0; i < this->num; i++) {
		for (int j = 0; j < 3; j++) {
			if (*current == '\0') {
				this->num = 0;
				return;
			}
			this->score[i].name[j] = *current++;
		}

		while (*current++ == ' ');
		current--;
		
		this->score[i].time = strtof(current, &end);
		if (current == end) {
			this->num = 0;
			return;
		}

		while ((*current != '\n') && (*current != '\0')) current++;

		if (*current != '\0')
			current++;
	}
}

Highscore::Highscore(const char *title) {
	this->num = 0;
	this->mutex_status = LWP_MutexInit(&this->mutex, true);
	
	strncpy(this->title, title, TITLE_SIZE);
	this->title[TITLE_SIZE - 1] = '\0';
	this->set_footer("Highscore unavailable");
}

Highscore::~Highscore() {
	LWP_MutexDestroy(this->mutex_status);
}

void *Highscore::load_online_thread_entry(void *t) {
	((Highscore *)t)->load_online_thread();
	return NULL;
}

void Highscore::load_online_thread() {
	this->set_footer("Initializing network");

	if (!start_network()) {
		this->set_footer("Network unavailable");
		return;
	}

	this->set_footer("Creating socket");

	s32 socket = net_socket(PF_INET, SOCK_STREAM, 0);
	if (socket < 0) {
		this->set_footer("Error creating socket");
		return;
	}

	this->set_footer("Set blocking flag");

	u32 non_blocking = 0;
	if (net_ioctl(socket, FIONBIO, (void *)&non_blocking) < 0) {
		net_close(socket);
		this->set_footer("Error setting blocking flag");
		return;
	}

	this->set_footer("Getting host IP");

	struct hostent *hostinfo = net_gethostbyname((char *)host_name);
	if (hostinfo == NULL) {
		net_close(socket);
		this->set_footer("Error getting host IP");
		return;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_len = sizeof(addr);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(host_port);
	addr.sin_addr.s_addr = *((u32*)hostinfo->h_addr_list[0]);
//	addr.sin_addr.s_addr = 0xc0a8166d;
//	addr.sin_addr.s_addr = 0x6d16a8c0;

	this->set_footer("Connecting");

	s32 result = net_connect(socket, (struct sockaddr *)&addr, sizeof(addr));
	if (result != 0) {
		net_close(socket);
		this->set_footer("Error connecting");
		return;
	}

	s32 bytes_sent = 0;
	
	this->set_footer("Sending request");

	while (bytes_sent < ((s32)sizeof(online_load) - 1)) {
		result = net_write(socket, &online_load[bytes_sent], sizeof(online_load) - bytes_sent - 1);

		if (result <= 0) {
			net_close(socket);
			this->set_footer("Write Error");
			return;
		}

		bytes_sent += result;
	}

	s32 bytes_read = 0;

	this->set_footer("Receiving data");

	char *online_buffer = new char[BUFFER_SIZE];
	if (online_buffer == NULL) {
		net_close(socket);
		this->set_footer("Error allocing buffer");
		return;
	}

	while ((result = net_read(socket, online_buffer + bytes_read, BUFFER_SIZE - bytes_read - 1))) {
		if (result < 0) {
			net_close(socket);
			this->set_footer("Read Error");
			delete [] online_buffer;
			return;
		}

		bytes_read += result;
	}

	net_close(socket);

	online_buffer[bytes_read] = '\0';

	this->set_footer("Checking integrity");

	char *response = new char[BUFFER_SIZE];
	if (response == NULL) {
		this->set_footer("Error allocing response buffer");
		delete [] online_buffer;
		return;
	}

	if (!this->parse_buffer(online_buffer, response)) {
		this->set_footer("Invalid response from server");
		delete [] online_buffer;
		delete [] response;
		return;
	}

	delete [] online_buffer;

	this->parse_response(response);

	delete [] response;

	this->set_footer("Successfully loaded");
}

void *Highscore::send_online_thread_entry(void *t) {
	struct send_argument_t *arg = (struct send_argument_t *)t;

	char temp[30];
	snprintf(temp, 30, "%g", arg->s.time * 100);
	
	arg->h->send_online_thread(arg->s.name, atoi(temp), arg->lt);

	delete arg;
	
	return NULL;
}

void Highscore::send_online_thread(const char *name, s32 time, Highscore *load_to) {
	this->set_footer("Initializing network");

	if (!start_network()) {
		this->set_footer("Network unavailable");
		return;
	}

	this->set_footer("Creating socket");

	s32 socket = net_socket(PF_INET, SOCK_STREAM, 0);
	if (socket < 0) {
		this->set_footer("Error creating socket");
		return;
	}

	this->set_footer("Set blocking flag");

	u32 non_blocking = 0;
	if (net_ioctl(socket, FIONBIO, (void *)&non_blocking) < 0) {
		net_close(socket);
		this->set_footer("Error setting blocking flag");
		return;
	}

	this->set_footer("Getting host IP");

	struct hostent *hostinfo = net_gethostbyname((char *)host_name);
	if (hostinfo == NULL) {
		net_close(socket);
		this->set_footer("Error getting host IP");
		return;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_len = sizeof(addr);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(host_port);
	addr.sin_addr.s_addr = *((u32*)hostinfo->h_addr_list[0]);

	this->set_footer("Connecting");

	s32 result = net_connect(socket, (struct sockaddr *)&addr, sizeof(addr));
	if (result != 0) {
		net_close(socket);
		this->set_footer("Error connecting");
		return;
	}

	this->set_footer("Sending score");

	char *online_buffer = new char[BUFFER_SIZE];
	if (online_buffer == NULL) {
		net_close(socket);
		this->set_footer("Error allocing buffer");
		return;
	}

	result = snprintf(online_buffer, BUFFER_SIZE, online_save, name[0], name[1], name[2], time);
	online_buffer[BUFFER_SIZE - 1] = '\0';

	if (result < 0) {
		net_close(socket);
		this->set_footer("Error writing to buffer");
		delete [] online_buffer;
		return;
	}

	s32 total_bytes = strlen(online_buffer);

	if (total_bytes >= BUFFER_SIZE - 1) {
		net_close(socket);
		this->set_footer("Buffer to small");
		delete [] online_buffer;
		return;
	}

	s32 bytes_sent = 0;

	while (bytes_sent < total_bytes) {
		result = net_write(socket, &online_buffer[bytes_sent], total_bytes - bytes_sent);

		if (result <= 0) {
			net_close(socket);
			this->set_footer("Write Error");
			delete [] online_buffer;
			return;
		}

		bytes_sent += result;
	}

	s32 bytes_read = 0;

	this->set_footer("Receiving response");

	while ((result = net_read(socket, online_buffer + bytes_read, BUFFER_SIZE - bytes_read - 1))) {
		if (result < 0) {
			net_close(socket);
			this->set_footer("Read Error");
			delete [] online_buffer;
			return;
		}

		bytes_read += result;
	}

	net_close(socket);

	delete [] online_buffer;

	this->set_footer("Successfully sent");
	
	load_to->load_online();	
}

void get_filename(char *full_path, const char *filename) {
	if (filesystem_check_file(game_folder))
		sprintf(full_path, "%s/%s", game_folder, filename);
	else
		sprintf(full_path, "%s", filename);
}

void Highscore::send_online(int score, Highscore *load_to) {
	if (mutex_status < 0)
		return;

	Lock lock(this->mutex, this->mutex_status);

	if (this->score[score].time >= 6000.0)
		return;

	lwp_t thread;
	this->set_footer("Sending highscore");

	struct send_argument_t *arg = new struct send_argument_t;
	arg->h = this;
	arg->lt = load_to;
	for (int i = 0; i < 3; i++)
		arg->s.name[i] = this->get_char(score, i);
	arg->s.time = this->score[score].time;

	s32 result = LWP_CreateThread(&thread, &Highscore::send_online_thread_entry, (void *)arg, NULL, 0, 1);

	if (result)
		delete arg;
}

void Highscore::load_online() {
	if (mutex_status < 0)
		return;

	lwp_t thread;
	this->set_footer("Loading online highscore");
	LWP_CreateThread(&thread, &Highscore::load_online_thread_entry, (void *)this, NULL, 0, 1);
}

void Highscore::load_local() {
	Lock lock(this->mutex, this->mutex_status);

	this->num = 0;
	this->set_footer("Loading");

	if (!filesystem_is_working()) {
		sprintf(this->footer, "Local highscore disabled");
		return;
	}

	char filename[256];
	get_filename(filename, local_highscore_filename);
	
	FILE *f = fopen(filename, "rb");

	if (f == NULL) {
		this->set_footer("No file found");
		return;
	}

	u16 version;
	size_t t = fread(&version, sizeof(version), 1, f);
	if (t == 0) {
		this->set_footer("Corrupted file");
		fclose(f);
		return;
	}

	t = fread(&this->num, sizeof(this->num), 1, f);
	if (t == 0) {
		this->set_footer("Corrupted file");
		fclose(f);
		return;
	}

	if (this->num > NUM_HIGHSCORES) {
		this->set_footer("Corrupted file");
		fclose(f);
		return;
	}

	for (int i = 0; i < this->num; i++) {
		t = fread(this->score[i].name, sizeof(this->score[i].name[0]), 3, f);
		if (t != 3) {
			this->set_footer("Corrupted file");
			fclose(f);
			return;
		}

		t = fread(&this->score[i].time, sizeof(this->score[i].time), 1, f);
		if (t == 0) {
			this->set_footer("Corrupted file");
			fclose(f);
			return;
		}
	}

	this->set_footer("Successfully loaded");

	fclose(f);
}

void Highscore::save_local() {
	if (!filesystem_is_working())
		return;

	char filename[256];
	get_filename(filename, local_highscore_filename);

	FILE *f = fopen(filename, "wb");

	if (f == NULL) {
		this->set_footer("Can't save to SD");
		return;
	}

	u16 version = 0x0001;
	size_t t = fwrite(&version, sizeof(version), 1, f);
	if (t == 0) {
		this->set_footer("Can't save to SD");
		fclose(f);
		return;
	}

	Lock lock(this->mutex, this->mutex_status);

	t = fwrite(&this->num, sizeof(this->num), 1, f);
	if (t == 0) {
		this->set_footer("Can't save to SD");
		fclose(f);
		return;
	}

	for (int i = 0; i < this->num; i++) {
		t = fwrite(this->score[i].name, sizeof(this->score[i].name[0]), 3, f);
		if (t != 3) {
			this->set_footer("Can't save to SD");
			fclose(f);
			return;
		}

		t = fwrite(&this->score[i].time, sizeof(this->score[i].time), 1, f);
		if (t != 1) {
			this->set_footer("Can't save to SD");
			fclose(f);
			return;
		}
	}

	this->set_footer("Successfully saved");

	fclose(f);
}

int Highscore::add_score(const char *name, f32 time) {
	Lock lock(this->mutex, this->mutex_status);

	int result = -1;
	
	int i = 0;

	while (i < this->num) {
		if (this->score[i].time > time)
			break;
		i++;
	}
	
	if ((i < NUM_HIGHSCORES) && (i >= 0)) {
		int j = this->num - 1;
		
		while (j >= i) {
			this->score[j + 1].name[0] = this->score[j].name[0];
			this->score[j + 1].name[1] = this->score[j].name[1];
			this->score[j + 1].name[2] = this->score[j].name[2];
			this->score[j + 1].time = this->score[j].time;
			j--;
		}
		
		this->score[i].name[0] = name[0];
		this->score[i].name[1] = name[1];
		this->score[i].name[2] = name[2];
		this->score[i].time = time;
		
		if (this->num < NUM_HIGHSCORES)
			this->num++;
			
		result = i;
	}
	
	return result;
}

void Highscore::draw_board(f32 x, f32 y, int naming, int letter) {
	Lock lock(this->mutex, this->mutex_status);

	int height = LINE_HEIGHT;          // Title
	height += LINE_HEIGHT;             // Footer
	if (this->num > 0) {
		height += LINE_HEIGHT;           // Header
		height += LINE_PADDING;          // Last line spacing
		height += FONT_SIZE * this->num; // Scores
	}

	// Draws a black rectangle to clear the area
	GX_ClearVtxDesc();
	GX_SetVtxDesc(GX_VA_POS, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT);

	GX_Begin(GX_QUADS, GX_VTXFMT0, 4);
		GX_Position3f32(x, -y, 0.0);
		GX_Color4u8(0, 0, 0, 255);
		GX_Position3f32(x, -y + height, 0.0);
		GX_Color4u8(0, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y + height, 0.0);
		GX_Color4u8(0, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y, 0.0);
		GX_Color4u8(0, 0, 0, 255);
	GX_End();

	GX_ClearVtxDesc();
	GX_SetVtxDesc(GX_VA_POS, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_TEX0, GX_DIRECT);

	DR_draw_text(x + (LINE_WIDTH / 2), y - LINE_HEIGHT + LINE_PADDING, (GXColor){ 255, 255, 255, 255 }, FTGX_JUSTIFY_CENTER, "%s", this->title);
	DR_draw_text(x + (LINE_WIDTH / 2), y - height + LINE_PADDING, (GXColor){ 255, 255, 255, 255 }, FTGX_JUSTIFY_CENTER, "%s", this->footer);

	// Write the scores
	if (this->num > 0) {
		GXColor color;

		DR_draw_text(x + 20, y - 2 * LINE_HEIGHT + LINE_PADDING, (GXColor){ 255, 255, 255, 255}, "Rank");
		DR_draw_text(x + 120, y - 2 * LINE_HEIGHT + LINE_PADDING, (GXColor){ 255, 255, 255, 255}, "Initials");
		DR_draw_text(x + 270, y - 2 * LINE_HEIGHT + LINE_PADDING, (GXColor){ 255, 255, 255, 255}, "Time");
	
		for (int i = 0; i < this->num; i++) {
			color = (GXColor){ 255, 255, 255, 255};
			u16 style[3] = { FTGX_JUSTIFY_CENTER, FTGX_JUSTIFY_CENTER, FTGX_JUSTIFY_CENTER };
			
			if (naming == i) {
				color = (GXColor){ 150, 150, 150, 255};
				style[letter] |= FTGX_STYLE_UNDERLINE;
			}
	
			DR_draw_text(x + 20, y - 2 * LINE_HEIGHT - FONT_SIZE * (i + 1), color, "%hu", i + 1);
			for (int j = 0; j < 3; j++)
				DR_draw_text(x + 120 + 32 * j, y - 2 * LINE_HEIGHT - FONT_SIZE * (i + 1), color, style[j], "%c", this->score[i].name[j]);
			DR_draw_text(x + 270, y - 2 * LINE_HEIGHT - FONT_SIZE * (i + 1), color, "%.2f", this->score[i].time);
		}
	}

	// Draws red borders at our table
	GX_ClearVtxDesc();
	GX_SetVtxDesc(GX_VA_POS, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT);

	GX_Begin(GX_LINESTRIP, GX_VTXFMT0, 5);
		GX_Position3f32(x, -y, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x, -y + height, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y + height, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x, -y, 0.0);
		GX_Color4u8(255, 0, 0, 255);
	GX_End();

	GX_Begin(GX_LINES, GX_VTXFMT0, 6);
		GX_Position3f32(x, -y + LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y + LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x, -y + 2 * LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y + 2 * LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x, -y + height - LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
		GX_Position3f32(x + LINE_WIDTH, -y + height - LINE_HEIGHT, 0.0);
		GX_Color4u8(255, 0, 0, 255);
	GX_End();

	GX_ClearVtxDesc();
	GX_SetVtxDesc(GX_VA_POS, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_CLR0, GX_DIRECT);
	GX_SetVtxDesc(GX_VA_TEX0, GX_DIRECT);
}

void Highscore::set_footer(const char *s) {
	Lock lock(this->mutex, this->mutex_status);
	strncpy(this->footer, s, FOOTER_SIZE);
	this->footer[FOOTER_SIZE - 1] = '\0';
}

u8 Highscore::get_char(int i, int l) {
	Lock lock(this->mutex, this->mutex_status);

	if ((i >= 0) && (i < this->num) && (l >= 0) && (l < 3))
		return this->score[i].name[l];
	return 0;
}

void Highscore::set_char(int i, int l, u8 c) {
	Lock lock(this->mutex, this->mutex_status);

	if ((i >= 0) && (i < this->num) && (l >= 0) && (l < 3) && (c >= 'A') && (c <= 'Z'))
		this->score[i].name[l] = c;
}
